﻿using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Nacos.V2;
using Nacos.V2.DependencyInjection;
using Nacos.V2.Exceptions;
using Nacos.V2.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Ocelot.Provider.Nacos;

public static class NacosConfigurationExtensions
{
    /// <summary>
    /// 替换配置源
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="configuration"></param>
    /// <param name="client"></param>
    /// <param name="loggerFactory"></param>
    /// <param name="parser"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static IConfigurationBuilder AddNacosV2Configuration(this IConfigurationBuilder builder, IConfiguration configuration,
       INacosConfigService client = null, ILoggerFactory loggerFactory = null, INacosConfigurationParser parser = null)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        if (configuration == null)
        {
            throw new ArgumentNullException(nameof(configuration));
        }

        var source = new NacosV2ConfigurationSource(null, null);
        configuration.Bind(source);
        source.NacosConfigurationParser = parser ?? DefaultJsonConfigurationStringParser.Instance;

        BuildDISource(source, client, loggerFactory);

        return builder.Add(source);
    }

    private static void BuildDISource(NacosV2ConfigurationSource source, INacosConfigService client, ILoggerFactory logFactory)
    {
        if (client == null)
        {
            IServiceCollection serviceCollection = new ServiceCollection();

            var sdkAction = source.GetNacosSdkOptions();
            serviceCollection.AddNacosV2Config(sdkAction);
            serviceCollection.AddLogging((x => x.AddConsole()));

            var serviceProvider = serviceCollection.BuildServiceProvider();

            client = serviceProvider.GetService<INacosConfigService>();
            logFactory = serviceProvider.GetService<ILoggerFactory>();
        }

        source.Client = client ?? throw new NacosException("Can't get INacosConfigService instance from DI Container");
        source.LoggerFactory = logFactory;
    }
}

public class NacosV2ConfigurationSource : NacosSdkOptions, IConfigurationSource
{
    /// <summary>
    /// The INacosConfigService.
    /// </summary>
    internal INacosConfigService Client;

    /// <summary>
    /// The ILoggerFactory.
    /// </summary>
    internal ILoggerFactory LoggerFactory;

    /// <summary>
    /// The configuration listeners
    /// </summary>
    public List<ConfigListener> Listeners { get; set; }

    /// <summary>
    /// The configuration parser, default is json
    /// </summary>
    public INacosConfigurationParser NacosConfigurationParser { get; set; }

    public NacosV2ConfigurationSource(INacosConfigService client, ILoggerFactory loggerFactory)
    {
        Client = client;
        LoggerFactory = loggerFactory;
    }

    /// <summary>
    /// Build the provider
    /// </summary>
    /// <param name="builder">builder</param>
    /// <returns>IConfigurationProvider</returns>
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new NacosV2ConfigurationProvider(this, Client, LoggerFactory);
    }

    public string GetNamespace()
    {
        // breaking change here after 1.3.3 release
        // do not use tenant any more!!!!
        if (Namespace.IsNotNullOrWhiteSpace())
        {
            return Namespace;
        }

        return string.Empty;
    }

    internal Action<NacosSdkOptions> GetNacosSdkOptions()
    {
        Action<NacosSdkOptions> action = x =>
        {
            x.ServerAddresses = ServerAddresses;
            x.Namespace = Namespace;
            x.AccessKey = AccessKey;
            x.ContextPath = ContextPath;
            x.EndPoint = EndPoint;
            x.DefaultTimeOut = DefaultTimeOut;
            x.SecretKey = SecretKey;
            x.Password = Password;
            x.UserName = UserName;
            x.ListenInterval = ListenInterval;
            x.ConfigUseRpc = ConfigUseRpc;
            x.ConfigFilterAssemblies = ConfigFilterAssemblies;
            x.ConfigFilterExtInfo = ConfigFilterExtInfo;
        };

        return action;
    }
}

public class ConfigListener
{
    /// <summary>
    /// Determines if the Nacos Server is optional
    /// </summary>
    public bool Optional { get; set; }

    /// <summary>
    /// Configuration ID
    /// </summary>
    public string DataId { get; set; }

    /// <summary>
    /// Configuration group
    /// </summary>
    public string Group { get; set; }
}

internal class NacosV2ConfigurationProvider : ConfigurationProvider, IDisposable
{
    private readonly NacosV2ConfigurationSource _configurationSource;

    private readonly INacosConfigurationParser _parser;

    private readonly INacosConfigService _client;

    private readonly ConcurrentDictionary<string, string> _configDict;

    private readonly Dictionary<string, MsConfigListener> _listenerDict;

    private readonly ILogger _logger;

    public NacosV2ConfigurationProvider(NacosV2ConfigurationSource configurationSource, INacosConfigService client,
        ILoggerFactory loggerFactory)
    {
        _configurationSource = configurationSource;
        _parser = configurationSource.NacosConfigurationParser;
        _configDict = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        _listenerDict = new Dictionary<string, MsConfigListener>();

        _client = client;
        _logger = loggerFactory?.CreateLogger<NacosV2ConfigurationProvider>();

        if (configurationSource.Listeners != null && configurationSource.Listeners.Any())
        {
            var tasks = new List<Task>();

            foreach (var item in configurationSource.Listeners)
            {
                var listener = new MsConfigListener(item.DataId, item.Group, item.Optional, this, _logger);

                tasks.Add(_client.AddListener(item.DataId, item.Group, listener));

                _listenerDict.Add($"{item.DataId}#{item.Group}", listener);
            }

            Task.WaitAll(tasks.ToArray());
        }
        else
        {
            // after remove old v1 code, Listeners must be not empty
            throw new NacosException("Listeners is empty!!");
        }
    }

    internal IDictionary<string, string> GetData() => Data;

    public void Dispose()
    {
        var tasks = new List<Task>();

        foreach (var item in _listenerDict)
        {
            var arr = item.Key.Split('#');
            var dataId = arr[0];
            var group = arr[1];

            tasks.Add(_client.RemoveListener(dataId, group, item.Value));
        }

        Task.WaitAll(tasks.ToArray());

        _logger?.LogInformation("Remove All Listeners");
    }

    public override void Load()
    {
        try
        {
            if (_configurationSource.Listeners != null && _configurationSource.Listeners.Any())
            {
                var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

                foreach (var listener in _configurationSource.Listeners)
                {
                    try
                    {
                        var config = _client.GetConfig(listener.DataId, listener.Group, 3000).ConfigureAwait(false).GetAwaiter()
                            .GetResult();

                        _configDict.AddOrUpdate($"{_configurationSource.GetNamespace()}#{listener.Group}#{listener.DataId}",
                            config, (x, y) => config);

                        var data = _parser.Parse(config);

                        foreach (var item in data)
                        {
                            dict[item.Key] = item.Value;
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger?.LogWarning(ex, "MS Config Query config error, dataid={0}, group={1}, tenant={2}",
                            listener.DataId, listener.Group, _configurationSource.GetNamespace());
                        if (!listener.Optional)
                        {
                            throw;
                        }
                    }
                }

                Data = dict;
            }
            else
            {
                // after remove old v1 code, Listeners must be not empty
                throw new NacosException("Listeners is empty!!");
            }
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Load config error");
        }
    }

    // for test
    internal void SetListener(string key, MsConfigListener listener)
    {
        _listenerDict[key] = listener;
    }

    internal class MsConfigListener : IListener
    {
        private readonly string _dataId;
        private readonly string _group;
        private readonly bool _optional;
        private readonly NacosV2ConfigurationProvider _provider;
        private readonly string _key;
        private readonly ILogger _logger;

        internal MsConfigListener(string dataId, string group, bool optional, NacosV2ConfigurationProvider provider,
            ILogger logger)
        {
            _dataId = dataId;
            _group = group;
            _optional = optional;
            _provider = provider;
            _logger = logger;
            _key = $"{provider._configurationSource.GetNamespace()}#{_group}#{_dataId}";
        }


        public void ReceiveConfigInfo(string configInfo)
        {
            _logger?.LogDebug("MsConfigListener Receive ConfigInfo 【{0}】", configInfo);
            try
            {
                _provider._configDict.AddOrUpdate(_key, configInfo, (x, y) => configInfo);

                var nData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

                foreach (var listener in _provider._configurationSource.Listeners)
                {
                    var key = $"{_provider._configurationSource.GetNamespace()}#{listener.Group}#{listener.DataId}";

                    if (!_provider._configDict.TryGetValue(key, out var config))
                    {
                        continue;
                    }

                    var data = _provider._parser.Parse(config);

                    foreach (var item in data)
                    {
                        nData[item.Key] = item.Value;
                    }
                }

                _provider.Data = nData;
                _provider.OnReload();
            }
            catch (Exception ex)
            {
                _logger?.LogWarning(ex, "call back reload config error");
                if (!_optional)
                {
                    throw;
                }
            }
        }
    }
}

internal class DefaultJsonConfigurationStringParser : INacosConfigurationParser
{
    private DefaultJsonConfigurationStringParser()
    {
    }

    internal static DefaultJsonConfigurationStringParser Instance = new DefaultJsonConfigurationStringParser();

    private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    private readonly Stack<string> _context = new Stack<string>();
    private string _currentPath;

    private JsonDocument _jsonDocument;

    public IDictionary<string, string> Parse(string input) => new DefaultJsonConfigurationStringParser().ParseString(input);

    private IDictionary<string, string> ParseString(string input)
    {
        _data.Clear();
        _jsonDocument = JsonDocument.Parse(input);

        var rootElement = _jsonDocument.RootElement;

        VisitJsonElement(rootElement);

        return _data;
    }

    private void VisitJsonElement(JsonElement element)
    {
        if (element.ValueKind == JsonValueKind.Object)
        {
            foreach (var property in element.EnumerateObject())
            {
                EnterContext(property.Name);
                VisitJsonElement(property.Value);
                ExitContext();
            }
        }
        else if (element.ValueKind == JsonValueKind.Array)
        {
            VisitJsonArray(element);
        }
        else
        {
            VisitJsonValue(element);
        }
    }

    private void VisitJsonArray(JsonElement array)
    {
        for (var index = 0; index < array.GetArrayLength(); index++)
        {
            EnterContext(index.ToString());
            VisitJsonElement(array[index]);
            ExitContext();
        }
    }

    private void VisitJsonValue(JsonElement value)
    {
        var key = _currentPath;

        if (_data.ContainsKey(key))
        {
            throw new FormatException($"A duplicate key '{key}' was found.");
        }

        if (value.ValueKind == JsonValueKind.String)
        {
            var unescapedValue = value.GetString()?.Replace("\\", ""); // 移除转义字符
            _data[key] = unescapedValue;
        }
        else
        {
            _data[key] = value.GetRawText();
        }
    }

    private void EnterContext(string context)
    {
        _context.Push(context);
        _currentPath = ConfigurationPath.Combine(_context.Reverse());
    }

    private void ExitContext()
    {
        _context.Pop();
        _currentPath = ConfigurationPath.Combine(_context.Reverse());
    }
}